package edu.amm.ui;

import edu.amm.common.StringUtils;
import edu.amm.io.IOUtils;
import edu.amm.neural.KohonenGrossbergRecognizer;
import edu.amm.neural.PatternClassPair;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ComboBox;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.layout.AnchorPane;
import javafx.stage.FileChooser;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;

/**
 * Контроллер формы приложения.
 *
 * @author Иванов Илья
 * @since 2014.03.04
 */
public class RecognizerController implements Initializable {

	/*
	private static final int CANVAS_WIDTH = 160;
	private static final int CANVAS_HEIGHT = 160;
	*/

	private static final int CANVAS_WIDTH = 240;
	private static final int CANVAS_HEIGHT = 80;

	private static final Color LINE_COLOR = Color.BLACK;
	private static final Color BACK_COLOR = Color.WHITE;

	private static final int LINE_WIDTH = 2;

	private static final int PATH_POINTS_COUNT = 4;

	private static final int PATTERN_DIM = GraphicsUtils.getDigitizedVectorLength(CANVAS_WIDTH, CANVAS_HEIGHT);

	@FXML
	private AnchorPane canvasPane;
	@FXML
	private ComboBox<String> classesCombo;

	private Canvas canvas;
	private GraphicsContext context;

	private int counter;
	private int[][] points;
	private boolean isPressed;

	private FileChooser fileChooser = new FileChooser();

	private KohonenGrossbergRecognizer recognizer;
	private boolean fromScratch = true;

	@Override
	public void initialize(URL url, ResourceBundle resourceBundle) {
		initCanvas();
		initClassesCombo();
		initFileChooser();
	}

	@FXML
	public void openPattern() {
		fileChooser.getExtensionFilters().clear();
		fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Portable Network Graphics", "*.png"));

		File file = fileChooser.showOpenDialog(canvasPane.getScene().getWindow());
		if (file == null) {
			MessageBox.show("Файл не выбран", MessageBox.Kind.ERROR, MessageBox.ButtonsType.OK);
			return;
		}

		fileChooser.setInitialDirectory(file.getParentFile());

		WritableImage writableImage = GraphicsUtils.readPng(file);
		if (writableImage == null) {
			MessageBox.show("Невозможно прочитать изображение", MessageBox.Kind.ERROR, MessageBox.ButtonsType.OK);
			return;
		}

		clearCanvas();
		context.drawImage(writableImage, 0, 0);
	}

	@FXML
	public void savePatternAs() {
		String className = classesCombo.getSelectionModel().getSelectedItem();
		if (StringUtils.isNullOrEmpty(className)) {
			MessageBox.show("Выберите класс, к которому принадлежит образ", MessageBox.Kind.ERROR, MessageBox.ButtonsType.OK);
			return;
		}

		fileChooser.getExtensionFilters().clear();
		fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Portable Network Graphics", "*.png"));

		File file = fileChooser.showSaveDialog(canvasPane.getScene().getWindow());
		if (file == null) {
			MessageBox.show("Файл не выбран", MessageBox.Kind.ERROR, MessageBox.ButtonsType.OK);
			return;
		}

		fileChooser.setInitialDirectory(file.getParentFile());

		String path = file.getPath();
		int slashIndex = path.lastIndexOf('\\');
		if (slashIndex >= 0) {
			path = path.substring(0, slashIndex + 1) + className + "_" + path.substring(slashIndex + 1);
		} else {
			path = className + "_" + path;
		}

		int dotIndex = path.lastIndexOf('.');
		if (dotIndex >= 0 && slashIndex < dotIndex) {
			path = path.substring(0, dotIndex) + ".png";
		} else {
			path += ".png";
		}

		WritableImage writableImage = canvas.snapshot(null, null);
		GraphicsUtils.saveAsPng(writableImage, new File(path));
	}

	@FXML
	public void trainOnCurrent() {
		int selectedClassIndex = classesCombo.getSelectionModel().getSelectedIndex();
		if (selectedClassIndex < 0) {
			MessageBox.show("Выберите класс, к которому принадлежит образ", MessageBox.Kind.ERROR, MessageBox.ButtonsType.OK);
			return;
		}

		double[] digitized = digitizeCurrentPattern();

		if (fromScratch) {
			recognizer.reset();
			fromScratch = false;
		}
		recognizer.train(new PatternClassPair(digitized, selectedClassIndex));
	}

	@FXML
	public void trainOnSet() {
		if (classesCombo.getItems() == null || classesCombo.getItems().isEmpty()) {
			MessageBox.show("Не загружены имена классов образов", MessageBox.Kind.ERROR, MessageBox.ButtonsType.OK);
			return;
		}

		fileChooser.getExtensionFilters().clear();
		fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Portable Network Graphics", "*.png"));

		List<File> files = fileChooser.showOpenMultipleDialog(canvasPane.getScene().getWindow());
		if (files == null || files.isEmpty()) {
			MessageBox.show("Файлы не выбраны", MessageBox.Kind.ERROR, MessageBox.ButtonsType.OK);
			return;
		}

		List<PatternClassPair> pairs = new ArrayList<PatternClassPair>();
		for (File file : files) {
			String path = file.getPath();

			int slashIndex = path.lastIndexOf('\\');
			if (slashIndex >= 0) {
				path = path.substring(slashIndex + 1);
			}

			int underscoreIndex = path.indexOf('_');
			if (underscoreIndex < 0) {
				MessageBox.show("Файл с изображением имеет некорректный формат имени: " + file.getPath(), MessageBox.Kind.ERROR, MessageBox.ButtonsType.OK);
				return;
			}

			String className = path.substring(0, underscoreIndex);
			int classNumber = classesCombo.getItems().indexOf(className);
			if (classNumber < 0) {
				MessageBox.show("Имя файла с изображением содержит неизвестное имя класса: " + file.getPath(), MessageBox.Kind.ERROR, MessageBox.ButtonsType.OK);
				return;
			}

			WritableImage image = GraphicsUtils.readPng(file);
			if (image == null) {
				MessageBox.show("Невозможно прочитать изображение " + file.getPath(), MessageBox.Kind.ERROR, MessageBox.ButtonsType.OK);
				return;
			}

			int digitizedLength = GraphicsUtils.getDigitizedVectorLength((int) image.getWidth(), (int) image.getHeight());
			if (digitizedLength != PATTERN_DIM) {
				MessageBox.show("Изображение имеет некорректную размерность: " + file.getPath(), MessageBox.Kind.ERROR, MessageBox.ButtonsType.OK);
				return;
			}

			GraphicsUtils.maximize(image, LINE_COLOR);

			boolean[] digitized = GraphicsUtils.digitize(image, LINE_COLOR);
			double[] digitizedDouble = toDoubleArray(digitized);

			pairs.add(new PatternClassPair(digitizedDouble, classNumber));
		}

		if (fromScratch) {
			recognizer.reset();
			fromScratch = false;
		}

		recognizer.train(pairs);
	}

	@FXML
	public void loadClassesNames(ActionEvent event) {
		fileChooser.getExtensionFilters().clear();
		fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Текстовый файл", "*.txt"));

		File file = fileChooser.showOpenDialog(canvasPane.getScene().getWindow());
		if (file == null) {
			MessageBox.show("Файл не выбран", MessageBox.Kind.ERROR, MessageBox.ButtonsType.OK);
			return;
		}

		fileChooser.setInitialDirectory(file.getParentFile());

		List<String> classesNames = IOUtils.readNonEmptyStrings(file.getPath());
		if (classesNames == null) {
			MessageBox.show("Невозможно прочитать выбранный файл", MessageBox.Kind.ERROR, MessageBox.ButtonsType.OK);
			return;
		}

		if (classesNames.isEmpty()) {
			MessageBox.show("В выбранном файле отсутствуют непустые имена классов", MessageBox.Kind.ERROR, MessageBox.ButtonsType.OK);
			return;
		}

		classesCombo.getItems().clear();
		for (String className : classesNames) {
			classesCombo.getItems().add(className.trim());
		}

		recognizer = new KohonenGrossbergRecognizer(PATTERN_DIM, classesNames.size());
		fromScratch = true;
	}

	@FXML
	public void startFromScratch() {
		fromScratch = true;
	}

	@FXML
	public void recognize() {
		List<String> classesNames = classesCombo.getItems();
		if (classesNames == null || classesNames.isEmpty()) {
			MessageBox.show("Не загружены имена классов образов", MessageBox.Kind.ERROR, MessageBox.ButtonsType.OK);
			return;
		}

		double[] digitized = digitizeCurrentPattern();
		int recognizedClassIndex = recognizer.classify(digitized);

		if (recognizedClassIndex < 0 || recognizedClassIndex >= classesNames.size()) {
			MessageBox.show("Невозможно распознать образ", MessageBox.Kind.ERROR, MessageBox.ButtonsType.OK);
			return;
		}

		String className = classesNames.get(recognizedClassIndex);
		MessageBox.show("Распознанный класс: " + className, MessageBox.Kind.INFO, MessageBox.ButtonsType.OK);
	}

	@FXML
	public void scale() {
		WritableImage image = canvas.snapshot(null, null);
		GraphicsUtils.maximize(image, LINE_COLOR);

		clearCanvas();
		context.drawImage(image, 0, 0);
	}

	@FXML
	private void clearCanvas() {
		GraphicsUtils.clear(canvas, BACK_COLOR);
	}

	@FXML
	public void exit() {
		Platform.exit();
	}

	private void initCanvas() {
		canvas = new Canvas(CANVAS_WIDTH, CANVAS_HEIGHT);
		canvas.setOnMousePressed(new EventHandler<MouseEvent>() {

			@Override
			public void handle(MouseEvent event) {
				pressed(event.getX(), event.getY());
			}
		});
		canvas.setOnMouseDragged(new EventHandler<MouseEvent>() {

			@Override
			public void handle(MouseEvent event) {
				dragged(event.getX(), event.getY());
			}
		});
		canvas.setOnMouseReleased(new EventHandler<MouseEvent>() {

			@Override
			public void handle(MouseEvent event) {
				released();
			}
		});
		canvasPane.getChildren().add(canvas);
		AnchorPane.setLeftAnchor(canvas, 3.0);
		AnchorPane.setTopAnchor(canvas, 3.0);

		context = canvas.getGraphicsContext2D();
		context.setStroke(LINE_COLOR);
		context.setLineWidth(LINE_WIDTH);

		clearCanvas();

		points = new int[PATH_POINTS_COUNT][2];
	}

	private void pressed(double x, double y) {
		isPressed = true;
		counter = -1;
		dragged(x, y);
	}

	private void dragged(double x, double y) {
		if (!isPressed) {
			return;
		}

		int i = (int) x;
		int j = (int) y;
		if (i < 0 || i >= CANVAS_WIDTH || j < 0 || j >= CANVAS_HEIGHT) {
			counter = -1;
			return;
		}

		counter++;
		int modulo = counter % points.length;

		points[modulo][0] = i;
		points[modulo][1] = j;

		int n = points.length - 1;
		if (modulo == n) {
			drawCurve();
			points[0][0] = points[n][0];
			points[0][1] = points[n][1];
			counter++;
		}
	}

	private void drawCurve() {
		context.beginPath();
		context.moveTo(points[0][0], points[0][1]);
		context.bezierCurveTo(points[1][0], points[1][1], points[2][0], points[2][1], points[3][0], points[3][1]);
		context.stroke();
	}

	private void released() {
		isPressed = false;
	}

	private void initClassesCombo() {
		classesCombo.getItems().clear();
	}

	private void initFileChooser() {
		try {
			fileChooser.setInitialDirectory(new File("./").getCanonicalFile());
		} catch (IOException e) {}
	}

	private double[] digitizeCurrentPattern() {
		WritableImage current = canvas.snapshot(null, null);

		GraphicsUtils.maximize(current, LINE_COLOR);

		boolean[] digitized = GraphicsUtils.digitize(current, LINE_COLOR);
		double[] digitizedDouble = toDoubleArray(digitized);

		return digitizedDouble;
	}

	private double[] toDoubleArray(boolean[] booleanArray) {
		double[] doubleArray = new double[booleanArray.length];
		for (int i = 0; i < doubleArray.length; i++) {
			doubleArray[i] = booleanArray[i] ? 1 : 0;
		}

		return doubleArray;
	}
}
